using System;
using System.IO;
using System.Configuration;
using System.Linq;
using System.Text;
using BSDS.Core.Infrastructure.Logging;
using BSDS.Core.Model.Data;
using BSDS.Core.Model.Services;
using BSDS.Core.Model.Services.Geocoding;
using BSDS.Core.Model.Services.Jobs;
using BSDS.Core.Model.Services.Jobs.Core;
using BSDS.Core.Model.Services.Uploading;
using BSDS.Core.Data.Local;

namespace BSDS.Commands.Commands.Implementation
{
    public class GeocodeCommand : CommandBase
    {
        private readonly GeocodingService geoService;
        private readonly DataRepository dataRepository;
        private readonly UploadService uploadService;
        private readonly AccountService accountService;
        private readonly JobRepository jobRepository;
        private readonly JobService jobService;
        private readonly Logger logger;
        private readonly GeocodeCommandConfiguration config;
        private readonly DateTime startTime;

        public override string Description
        {
            get { return CreateDescription(); }
        }

        public GeocodeCommand(GeocodingService geoService, AccountService accountService, DataRepository dataRepository,
                              UploadService uploadService, JobRepository jobRepository,
                              JobService jobService, Logger logger, GeocodeCommandConfiguration config)
        {
            this.geoService = geoService;
            this.accountService = accountService;
            this.dataRepository = dataRepository;
            this.uploadService = uploadService;
            this.jobRepository = jobRepository;
            this.jobService = jobService;
            this.logger = logger;

            this.config = config;

            startTime = DateTime.Now;
        }

        public override CommandResult Execute(ExecutionContext context)
        {
            config.Validate();

            if (!string.IsNullOrEmpty(config.DropFilenamePrefix) && !Path.GetFileNameWithoutExtension(config.InputFilename).StartsWith(config.DropFilenamePrefix))
            {
                string message = "Filename must start with \"" + config.DropFilenamePrefix + "\" : " + config.InputFilename;
                logger.Log("File Ingored. " + message);

                return CommandResult.Error("Unsupported file was dropped", message);
            }

            var data = dataRepository.LoadFromFile(config.InputFilename);
            if (!String.IsNullOrEmpty(config.EntityTypeName))
            {
                data.EntityTypeName = config.EntityTypeName;
            }

            string geocodeOverridesTable = "GeocodeOverrides.csv";

            // Load the geocode override table
            if (File.Exists(geocodeOverridesTable)) 
            {
                var geocodeEntities = dataRepository.LoadFromFile(geocodeOverridesTable).Entities;
                // Insert geocode values into the original dataset
                data.UpdateGeocoding(geocodeEntities);
                logger.Log("Loaded " + geocodeEntities.Count() + " entities from Geocode Override table: " + geocodeOverridesTable);
            }
            logger.Log("Geocode overrides table not found (skipping): " + geocodeOverridesTable);

            JobState jobState;

            if (config.ResumeCompletedJob)
            {
                jobState = geoService.QueryState(config.CompletedJobId);
                return ProcessGeocodingResults(data, jobState);
            }

            if (!data.GeocodingNeeded(config.GeocodingMode))
            {
                logger.Log("No entities for geocoding. All geoinformation already available.");

                UploadIfRequested(config.InputFilename);

                return CommandResult.Success();
            }

            var job = geoService.ScheduleGeocoding(data, config.GeocodingMode);
            job.Command = context.CommandInfo.ToString();
            jobRepository.Save(job);

            jobState = jobService.WaitForEnd(job.Id, geoService);

            return ProcessGeocodingResults(data, jobState);
        }

        private CommandResult ProcessGeocodingResults(BsdsData data, JobState jobState)
        {
            var commandResult = CommandResult.Success();
            var geocodingDetails = new StringBuilder();

            if (!jobState.DataAvailable)
            {
                var failureDetails = geoService.GetFailureDetials(jobState);
                logger.Log(string.Format("Failure: {0}.", failureDetails));
                return CommandResult.Error("Geocoding error.", failureDetails);
            }

            var job = jobRepository.Get<GeocodeJob>(jobState.Id);
            var BSDSGeocodingResult = geoService.DownloadGecodingResults(job.Id);
            var BSDSGeocodedEntities = BSDSGeocodingResult.GetAllEntities();

            if (job.Mode == GeocodingMode.Reverse)
                data.UpdateReverseGeocoding(BSDSGeocodedEntities);
            else
                data.UpdateGeocoding(BSDSGeocodedEntities);

            var impreciseOrBadGeocodeEntities = new GeocodingResult(BSDSGeocodingResult.GetImpreciseOrBadEntities());
            var regeocodeTheseEntities = data.GetMatchingEntities(impreciseOrBadGeocodeEntities);

            // Try to improve the geocoding by using the Bing Maps Locations API
            var LocationsAPIGeocodingResult = geoService.GeocodeEntitiesIndividually(regeocodeTheseEntities);
            var regeocodedEntites = LocationsAPIGeocodingResult.GetAllEntities();

            if (job.Mode == GeocodingMode.Reverse)
                data.UpdateReverseGeocoding(regeocodedEntites);
            else
                data.UpdateGeocoding(regeocodedEntites);

            // The logic below assumes we have re-geocoded all imprecise or failing entities
            if (!LocationsAPIGeocodingResult.AllEntitiesHighConfidence())
            {
                geocodingDetails.Append("IMPRECISE ENTITIES:").Append(Environment.NewLine);
                var impreciseEntitiesIds = LocationsAPIGeocodingResult.GetImpreciseEntities().Select(e => e.PrimaryKeyValue).ToArray();
                geocodingDetails.Append(string.Join(",", impreciseEntitiesIds)).Append(Environment.NewLine + Environment.NewLine);
                commandResult = CommandResult.Warning("Geocoding was imprecise or ambiguous for some entities", geocodingDetails.ToString());
            }

            if (!LocationsAPIGeocodingResult.AllEntitiesValid())
            {
                geocodingDetails.Append("FAILED ENTITIES:").Append(Environment.NewLine);
                var invalidEntitiesIds = LocationsAPIGeocodingResult.GetFailedEntities().Select(e => e.PrimaryKeyValue).ToArray();
                geocodingDetails.Append(string.Join(",", invalidEntitiesIds)).Append(Environment.NewLine + Environment.NewLine);
                // The following command result short message takes prececence, but we will provide cumultive detail results
                commandResult = CommandResult.Warning("Geocoding has failed for some entities", geocodingDetails.ToString());
            }

            logger.Log(geocodingDetails.ToString());
            logger.Log(string.Format("Saving geocoding results to {0}.", config.OutputFilename));

            var outputFormat = config.OutputFormat == DataFormat.OriginalFormat ? data.OriginalFormat : config.OutputFormat; 
            if (config.Portal)
            {
                if (outputFormat != DataFormat.Tab && outputFormat != DataFormat.Pipe)
                {
                    // TODO: Shouldn't we be checking this before doing any work?
                    throw new ArgumentException("/portal option can be used only with /format=tab or /format=pipe");
                }
                dataRepository.SaveInPortalFormat(data, config.OutputFilename, outputFormat);
            }
            else
            {
                dataRepository.SaveToFile(data, config.OutputFilename, outputFormat);
            }

            UploadIfRequested(config.OutputFilename);

            return commandResult;
        }

        private void UploadIfRequested(string filename)
        {
            if (config.UploadAfterMerging)
            {
                var uploadData = dataRepository.LoadFromFile(filename);
                var outputFormat = config.OutputFormat == DataFormat.OriginalFormat ? uploadData.OriginalFormat : config.OutputFormat; 
                var job = uploadService.ScheduleUpload(uploadData, config.DataSourceName, outputFormat, config.EntityTypeName, config.QueryKey);

                jobService.WaitForEnd(job.Id, uploadService);
            }
        }

        private string CreateDescription()
        {
            var builder = new StringBuilder();

            builder.Append("GEOCODE START TIME: " + startTime.ToLongTimeString()).Append(Environment.NewLine)
                   .Append("DATA SOURCE NAME: " + config.DataSourceName).Append(Environment.NewLine);

            if (config.UploadAfterMerging && accountService.DataSourceExists(config.DataSourceName))
            {
                string publishDate = accountService.GetDataSourceInformation(config.DataSourceName).LastUpdate;
                builder.Append("LAST PUBLISH DATE: " + publishDate);
            }

            return builder.ToString();
        }
    }

    public class GeocodeCommandConfiguration
    {
        public string InputFilename { get; private set; }
        public GeocodingMode GeocodingMode { get; private set; }
        public bool StoreInOutputFile { get; set; }
        public string OutputFilename { get; set; }
        public DataFormat OutputFormat { get; private set; }
        public bool Portal { get; set; }
        public bool UploadAfterMerging { get; private set; }
        public string DataSourceName { get; private set; }
        public string EntityTypeName { get; private set; }
        public string QueryKey { get; private set; }
        public bool ResumeCompletedJob { get; private set; }
        public string CompletedJobId { get; private set; }
        public string DropFilenamePrefix { get; private set; }

        public GeocodeCommandConfiguration(string inputFilename, GeocodingMode geocodingMode, bool storeInOutputFile, 
            string outputFilename, bool portal, DataFormat outputFormat, bool uploadAfterMerging, 
            string dataSourceName, string entityTypeName, string queryKey, bool resumeCompletedJob, string jobId,
            string dropFilenamePrefix)
        {
            InputFilename = inputFilename;
            GeocodingMode = geocodingMode;
            StoreInOutputFile = storeInOutputFile;
            OutputFilename = outputFilename;
            OutputFormat = outputFormat;
            Portal = portal;
            UploadAfterMerging = uploadAfterMerging;
            DataSourceName = dataSourceName;
            EntityTypeName = entityTypeName;
            QueryKey = queryKey;
            ResumeCompletedJob = resumeCompletedJob;
            CompletedJobId = jobId;
            DropFilenamePrefix = dropFilenamePrefix;
        }

        public void Validate()
        {
            if (string.IsNullOrEmpty(OutputFilename))
            {
                throw new ConfigurationErrorsException("/outfile parameter is required for /geocode command");
            }
        }
    }
}